/*
 * File:                StandardController.cs
 * Authors:             
 * Company:             Sandia National Laboratories
 * Project:             MVC
 *
 * Copyright 2008, Sandia Corporation.  Under the terms of Contract
 * DE-AC04-94AL85000, there is a non-exclusive license for use of this work by
 * or on behalf of the U.S. Government. Export of this program may require a
 * license from the United States Government. See CopyrightHistory.txt for
 * complete details.
 *
 * Reviewers:
 * Review Date:
 * Review Comments:
 *
 * Revision History:
 *
 * $Log: StandardController.cs,v $
 * Revision 1.41  2009/03/11 22:58:10  jtmccl
 * Plugins now support ExportTools.  Bug #938 [JTM20090310]
 *
 * Revision 1.40  2008/10/28 21:15:18  bstitus
 * Added a check for Logger.IsLoggingEnabled() around the code to construct the log string.  Bug #1260.
 *
 * Revision 1.39  2008/10/28 15:01:25  bstitus
 * Added calls to LoggingTools.SendToLog for all seven (two new - IImporter and IExporter) if in the Remove method, the item was not found in the collection.  Bug # 1259
 *
 * Revision 1.38  2008/10/28 14:42:14  bstitus
 * In StandardController, moved the instantiation of the previously moved List collections to AbstractStandardController's constructor.  Fixed the regions for IRequestRouter and IController.  Rearranged the order of Add/Remove methodes so they are together; Add then Remove.  Added implementations for the new Add/Remove methods for IImporter and IExplorer collections.  Bug #1254
 *
 * Revision 1.37  2008/10/24 19:31:48  bstitus
 * Converted the dictionary data types to Lists and updated the code because of the different methods for List.  Bug # 1256
 *
 * Revision 1.36  2008/10/23 21:29:38  bstitus
 * Reverted efforts earlier today to make a central exception logging method.  Instead, a constant string was created to reduce the proliferation in the code.  Bug #1262
 *
 * Revision 1.35  2008/10/23 21:04:11  bstitus
 * Changed the inheritance to use the new class, AbstractStandardController.  Also, moved the implementation of the Start method for the IStartable interface to AbstractStandardController.  Bug #1258
 *
 * Revision 1.34  2008/10/23 14:18:28  bstitus
 * Replaced the code for catching and handling exceptions with a call to LoggingTools' new method.  Bug # 1262
 *
 * Revision 1.33  2008/10/16 14:04:53  bstitus
 * Added carriage returns to put parameters on new lines per the comments from the code review.
 *
 * Revision 1.32  2008/10/14 21:04:13  bstitus
 * Updated the file based on the comments from the code review for bug #1255.
 *
 * Revision 1.31  2008/10/14 19:34:42  bstitus
 * Added the file header that JT sent me in resolution of bug #1252.
 *
 *
 */


using Microsoft.Practices.EnterpriseLibrary.ExceptionHandling;
using Microsoft.Practices.EnterpriseLibrary.Logging;

using Sandia.MVCFramework.Common;
using Sandia.MVCFramework.Data;
using Sandia.MVCFramework.DataAccessor;
using Sandia.MVCFramework.DataAnalyzer;
using Sandia.MVCFramework.DataStore;
using Sandia.MVCFramework.Request;
using Sandia.MVCFramework.View;
using Sandia.MVCFramework.ViewManager;

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace Sandia.MVCFramework.Controller
{
    /// <summary>
    /// Implementation of IController that provides
    /// standard functionality for keeping track of available
    /// DataAccessors and DataAnalyzers and that maps
    /// requests to actions
    /// </summary>
    public class StandardController: AbstractStandardController, IController
    {

        /// <summary>
        /// The mapping of requests to specific actions
        /// </summary>
        protected Dictionary<Type, List<IRequestProcessor>> requestRoutings;

        /// <summary>
        /// Used to synchronize access to this.requestRoutings. 
        /// </summary>
        /// <remarks>
        /// Allows multiple threads to read, or one thread to write at any given
        /// time. For more information, see the following URL:
        /// 
        /// http://msdn2.microsoft.com/en-us/library/system.threading.readerwriterlock.aspx
        /// 
        /// The real solution is not to use "lock", but rather to use
        /// the ReaderWriterLock class.  By using this class, publishes can 
        /// still happen asynchronously, since all they do is read 
        /// this.dataSubscriptions. But adding a removing subscriptions is
        /// controlled down to one thread at a time.        
        /// </remarks>
        protected ReaderWriterLock requestRoutingsReaderWriterLock;

        /// <summary>
        /// A mapping of threads to their corresponding sequence trackers.
        /// </summary>
        private Dictionary<Thread, ThreadSequenceTracker> sequenceThreads =
            new Dictionary<Thread, ThreadSequenceTracker>();

        /// <summary>
        /// A mapping of ThreadSequenceTrackers to progress bars that are
        /// associated with that sequence.
        /// </summary>
        private Dictionary<ThreadSequenceTracker, ProgressBarForm> progressBars =
            new Dictionary<ThreadSequenceTracker, ProgressBarForm>();

        /// <summary>
        /// Default constructor; initializes collections of known
        /// data accessors and data analyzers and mapping from
        /// requests to actions
        /// </summary>
        public StandardController()
        {
            this.requestRoutings = new Dictionary<Type, List<IRequestProcessor>>();

            this.requestRoutingsReaderWriterLock = new ReaderWriterLock();
        }

        /// <summary>
        /// Display the progress bar form
        /// </summary>
        /// <param name="progressBarForm"></param>
        void processShowProgressBar(
            object progressBarForm)
        {
            try
            {
                if (progressBarForm is ProgressBarForm)
                {
                    // Initialize form in critical section
                    lock (progressBarForm)
                    {
                        ((ProgressBarForm)progressBarForm).ShowWithInvokeCheck();
                        ((ProgressBarForm)progressBarForm).HideWithInvokeCheck();
                    }

                    // ShowDialog doesn't return until form dismissed, so can't lock on it
                    ((ProgressBarForm)progressBarForm).ShowDialogWithInvokeCheck();

                    lock (progressBarForm)
                    {
                        ((ProgressBarForm)progressBarForm).Dispose();
                    }
                }

            }
            catch (Exception e)
            {
                // Check to see if the exception should be re-thrown
                //
                if (ExceptionPolicy.HandleException(e, LoggingTools.LoggingPolicy))
                {
                    throw e;
                }
            }
        }

        /// <summary>
        /// A wrapper to process the request
        /// </summary>
        /// <param name="processRequestPackage"></param>
        void processRequestWrapper(
            object processRequestPackage)
        {
            try
            {
                if (processRequestPackage is DictionaryEntry)
                {
                    DictionaryEntry package = (DictionaryEntry)processRequestPackage;
                    IRequestProcessor requestProcessor = (IRequestProcessor)package.Key;
                    IRequest request = (IRequest)package.Value;

                    requestProcessor.ProcessRequest(request);
                }
            }
            catch (Exception e)
            {
                // Check to see if the exception should be re-thrown
                //
                if (ExceptionPolicy.HandleException(e, LoggingTools.LoggingPolicy))
                {
                    throw e;
                }
            }
        }

        /// <summary>
        /// Route the request
        /// </summary>
        /// <param name="request"></param>
        /// <param name="synchronous"></param>
        private void routeRequest(
            IRequest request, 
            bool synchronous)
        {
            // try routing to the type itself
            this.routeRequestToType(request.GetType(), request, synchronous);

            //Iterator over all the interfaces the type implements and see
            // if anyone wants that routing
            foreach (Type interfaceType in request.GetType().GetInterfaces())
            {
                this.routeRequestToType(interfaceType, request, synchronous);
            }

            //Walk up inheritance tree (all the way to the Object) to see
            //if anyone is subscribing to more generic parent types
            Type parentType = request.GetType().BaseType;
            while (parentType != null)
            {
                this.routeRequestToType(parentType, request, synchronous);
                parentType = parentType.BaseType;
            }
        }

        /// <summary>
        /// Internal common handler for request routing
        /// </summary>
        /// <param name="request">The request to route</param>
        /// <param name="synchronous">True for synchronous routing, false for asynchronous routing</param>
        /// <param name="requestType">The type to use for the routing.</param>
        private void routeRequestToType(Type requestType, IRequest request, bool synchronous)
        {
            ThreadSequenceTracker sequenceTracker = null;

            // If the current thread is already in the collections of threads
            // for this sequence, use the tracker for the thread, otherwise
            // make a new tracker for the thread, but don't add the thread
            // to the collection.
            if (this.sequenceThreads.ContainsKey(Thread.CurrentThread))
            {
                sequenceTracker = this.sequenceThreads[Thread.CurrentThread];
            }
            // Otherwise we make a new sequence for this request.
            else
            {
                sequenceTracker = new ThreadSequenceTracker();
                sequenceTracker.ThreadCreated += new OnThreadCreated(sequenceTracker_ThreadCreated);
                sequenceTracker.ThreadExecutionComplete += new OnThreadExecutionComplete(sequenceTracker_ThreadExecutionComplete);
                sequenceTracker.SequenceComplete+=new OnSequenceComplete(sequenceTracker_SequenceComplete);
            }

            // Aquire a reader lock for this.requestRoutings.
            this.requestRoutingsReaderWriterLock.AcquireReaderLock(Timeout.Infinite);
            try
            {
                // Lookup who handles the request and route it
                // appropriately, placing each routed copy of the
                // request on its own thread if requested
                if (this.requestRoutings.ContainsKey(requestType))
                {
                    List<IRequestProcessor> requestProcessors =
                        this.requestRoutings[requestType];

                    ProgressBarForm frm = null;
                    // Put up progress bar if requested
                    if (request.Parameters.ContainsKey(ShowProgressBarRequestParameter.NAME) &&
                        request[ShowProgressBarRequestParameter.NAME] != null &&
                        request[ShowProgressBarRequestParameter.NAME].Data.ToString().Length > 0)
                    {
                        frm = new ProgressBarForm();
                        frm.Text = request[ShowProgressBarRequestParameter.NAME].Data.ToString();
                        if (!synchronous)
                        {
                            this.progressBars[sequenceTracker] = frm;
                        }
                        WaitCallback waitCallback = new WaitCallback(this.processShowProgressBar);
                        ThreadPool.QueueUserWorkItem(waitCallback, frm);
                    }

                    foreach (IRequestProcessor requestProcessor in requestProcessors)
                    {
                        // Process the request
                        DictionaryEntry processRequestPackage =
                            new DictionaryEntry(requestProcessor, request);

                        if (synchronous)
                        {
                            this.processRequestWrapper(processRequestPackage);
                            // Close the progress bar form, if necessary, after
                            // processing the request.
                            if (frm != null)
                            {
                                frm.CloseWithInvokeCheck();
                            }
                        }
                        else
                        {
                            // Process the request on a new thread within the sequence.
                            sequenceTracker.AddThread(
                                this.processRequestWrapper,
                                processRequestPackage);
                        }

                        //Only perform the string creation if logging is enabled
                        //
                        if (Logger.IsLoggingEnabled())
                        {
                            // Generate a log entry
                            StringBuilder sb = new StringBuilder();
                            foreach (KeyValuePair<string, IRequestParameter> entry in request.Parameters)
                            {
                                // Heuristic to keep passwords out of logs
                                if (entry.Key.ToLower().Contains("password"))
                                {
                                    sb.Append("\n  " + entry.Key + ": ********");
                                }
                                else
                                {
                                    sb.Append("\n  " + entry.Key + ": ");
                                    if (entry.Value == null)
                                    {
                                        continue;
                                    }
                                    sb.Append((entry.Value.Data == null) ? "" : entry.Value.Data.ToString());
                                }
                            }

                            // Generate a log entry
                            LoggingTools.SendToLog(
                                "\nRequest of type " + requestType.ToString() +
                                "\nrouted to " + requestProcessor.ToString() +
                                "\nby " + this.GetType().ToString() +
                                "\nRequest Parameters:" + sb.ToString(),
                                LoggingCategories.REQUEST_ROUTING,
                                0);
                        }
                    }
                }
                // If we don't know how to route the request, just let
                // it die quietly, unrouted (but make a log entry)
                else
                {
                    // Generate a log entry
                    LoggingTools.SendToLog(
                        "Request of type " + requestType.ToString() +
                        " cannot be routed (no request processors) by " + this.GetType().ToString(),
                        LoggingCategories.REQUEST_ROUTING,
                        0);
                }
            }
            finally
            {
                // Ensure that the lock is released.
                this.requestRoutingsReaderWriterLock.ReleaseReaderLock();
            }
        }

        /// <summary>
        /// When the request processor is explicitly disposed, it
        /// needs to be removed from the request routings list.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void requestProcessor_Disposed(object sender, EventArgs e)
        {
            IRequestProcessor processor = sender as IRequestProcessor;
            if (processor == null) return;

            // Aquire a reader lock for this.requestRoutings.
            this.requestRoutingsReaderWriterLock.AcquireWriterLock(Timeout.Infinite);
            try
            {
                foreach (List<IRequestProcessor> processorList in requestRoutings.Values)
                {
                    if (processorList.Contains(processor))
                        processorList.Remove(processor);
                }
            }
            finally
            {
                // Ensure that the lock is released.
                this.requestRoutingsReaderWriterLock.ReleaseWriterLock();
            }
        }

        /// <summary>
        /// Called when a new thread is created in a sequence.
        /// </summary>
        /// <param name="sequenceTracker">The sequence tracker.</param>
        /// <param name="thread">The new thread.</param>
        void sequenceTracker_ThreadCreated(
            ThreadSequenceTracker sequenceTracker, 
            Thread thread)
        {
            lock (this.sequenceThreads)
            {
                this.sequenceThreads[thread] = sequenceTracker;
            }
        }

        /// <summary>
        /// Called when a thread finishes executing.
        /// </summary>
        /// <param name="sequenceTracker">The sequence tracker.</param>
        /// <param name="thread">The thread.</param>
        void sequenceTracker_ThreadExecutionComplete(
            ThreadSequenceTracker sequenceTracker, 
            Thread thread)
        {
            lock (this.sequenceThreads)
            {
                this.sequenceThreads.Remove(thread);
            }
        }

        /// <summary>
        /// Called when a sequence is complete.
        /// </summary>
        /// <param name="sequenceTracker">The sequence tracker.</param>
        void sequenceTracker_SequenceComplete(
            ThreadSequenceTracker sequenceTracker)
        {
            if (this.progressBars.ContainsKey(sequenceTracker))
            {
                this.progressBars[sequenceTracker].CloseWithInvokeCheck();
                this.progressBars.Remove(sequenceTracker);
            }
        }

        #region IRequestRouter Members

        /// <summary>
        /// Route a specified request (asynchronously)
        /// </summary>
        /// <param name="request">The request to route.</param>
        public void RouteRequest(
            IRequest request)
        {
            this.routeRequest(request, false);   
        }

        /// <summary>
        /// Route a specified request synchronously
        /// </summary>
        /// <param name="request">The request to route.</param>
        public void RouteRequestSynchronously (
            IRequest request)
        {
            this.routeRequest(request, true);
        }

        /// <summary>
        /// Adds a a request processor to the list of processors
        /// for a specified request
        /// </summary>
        /// <param name="requestType">The request type to trigger on</param>
        /// <param name="requestProcessor">The processor for the specified request type</param>
        public void AddRequestRouting(
            Type requestType, 
            IRequestProcessor requestProcessor)
        {
            // See if we already have actions set up
            // for the specified request

            List<IRequestProcessor> requestRoutings;

            // Aquire a reader lock for this.requestRoutings.
            this.requestRoutingsReaderWriterLock.AcquireWriterLock(Timeout.Infinite);
            try
            {
                if (this.requestRoutings.ContainsKey(requestType))
                {
                    // Add this action to the collection
                    // of existing action for this request
                    requestRoutings = this.requestRoutings[requestType];
                    if (!requestRoutings.Contains(requestProcessor))
                    {
                        requestProcessor.Disposed += new EventHandler(requestProcessor_Disposed);
                        requestRoutings.Add(requestProcessor);
                    }
                }
                else
                {
                    // No actions for this request yet, so
                    // set one up
                    requestProcessor.Disposed += new EventHandler(requestProcessor_Disposed);
                    requestRoutings = new List<IRequestProcessor>();
                    requestRoutings.Add(requestProcessor);
                    this.requestRoutings[requestType] = requestRoutings;
                }
            }
            finally
            {
                // Ensure that the lock is released.
                this.requestRoutingsReaderWriterLock.ReleaseWriterLock();
            }
        }

        /// <summary>
        /// Removes a request processor from the list of processors
        /// for a specified request
        /// </summary>
        /// <param name="requestType">The request type to remove from</param>
        /// <param name="requestProcessor">The processor to remove</param>
        public void RemoveRequestRouting(
            Type requestType,
            IRequestProcessor requestProcessor)
        {
            // Aquire a reader lock for this.requestRoutings.
            this.requestRoutingsReaderWriterLock.AcquireWriterLock(Timeout.Infinite);
            try
            {
                if (this.requestRoutings.ContainsKey(requestType)
                    && this.requestRoutings[requestType].Contains(requestProcessor))
                {
                    this.requestRoutings[requestType].Remove(requestProcessor);
                }
                else
                {
                    //Note that the item was not found in the collection
                    //
                    LoggingTools.SendToLog("RequestRouting for type: " + requestType.ToString() + ", was not found in the collection of requestRoutings.", LoggingCategories.GENERAL, 50);
                }
            }
            finally
            {
                // Ensure that the lock is released.
                this.requestRoutingsReaderWriterLock.ReleaseWriterLock();
            }
        }

        #endregion

        #region IController Members

        /// <summary>
        /// Adds a data accessor to the collection
        /// of data accessors the controller is aware of
        /// </summary>
        /// <param name="dataAccessor">The data accessor
        /// to add</param>
        public void AddDataAccessor(
            IDataAccessor dataAccessor)
        {
            lock (this.dataAccessors)
            {
                if (!this.dataAccessors.Contains(dataAccessor))
                {
                    this.dataAccessors.Add(dataAccessor);
                    dataAccessor.InitDataPacketProducer(this);
                }
            }
        }

        /// <summary>
        /// Removes a data accessor from the collection
        /// of data accessors the controller is aware of
        /// </summary>
        /// <param name="dataAccessor">The data accessor
        /// to remove</param>
        public void RemoveDataAccessor(
            IDataAccessor dataAccessor)
        {
            lock (this.dataAccessors)
            {
                if (this.dataAccessors.Contains(dataAccessor))
                {
                    this.dataAccessors.Remove(dataAccessor);
                }
                else
                {
                    //Note that the item was not found in the collection
                    //
                    LoggingTools.SendToLog("DataAccessor: " + dataAccessor.ToString() + ", was not found in the collection of dataAccessors.", LoggingCategories.GENERAL, 50);
                }
            }
        }

        /// <summary>
        /// Adds a data analyzer to the collection
        /// of data analyzers the controller is aware of
        /// </summary>
        /// <param name="dataAnalyzer">The data analyzer
        /// to add</param>
        public void AddDataAnalyzer(
            IDataAnalyzer dataAnalyzer)
        {
            lock (this.dataAnalyzers)
            {
                if (!this.dataAnalyzers.Contains(dataAnalyzer))
                {
                    this.dataAnalyzers.Add(dataAnalyzer);

                    dataAnalyzer.InitDataPacketProducer(this);

                    dataAnalyzer.InitRequestGenerator(this);
                }
            }
        }

        /// <summary>
        /// Removes a data analyzer from the collection
        /// of data analyzers the controller is aware of
        /// </summary>
        /// <param name="dataAnalyzer">The data analyzer
        /// to remove</param>
        public void RemoveDataAnalyzer(
            IDataAnalyzer dataAnalyzer)
        {
            lock (this.dataAnalyzers)
            {
                if (this.dataAnalyzers.Contains(dataAnalyzer))
                {
                    this.dataAnalyzers.Remove(dataAnalyzer);
                }
                else
                {
                    //Note that the item was not found in the collection
                    //
                    LoggingTools.SendToLog("DataAnalyzer: " + dataAnalyzer.ToString() + ", was not found in the collection of dataAnalyzers.", LoggingCategories.GENERAL, 50);
                }
            }
        }

        /// <summary>
        /// Adds a data store to the collection
        /// of data stores the controller is aware of
        /// </summary>
        /// <param name="dataStore">The data store
        /// to add</param>
        public void AddDataStore(
            IDataStore dataStore)
        {
            lock (this.dataStores)
            {
                if (!this.dataStores.Contains(dataStore))
                {
                    this.dataStores.Add(dataStore);
                    dataStore.InitDataPacketProducer(this);
                    dataStore.InitRequestGenerator(this);
                }
            }
        }

        /// <summary>
        /// Removes a data store from the collection
        /// of data stores the controller is aware of
        /// </summary>
        /// <param name="dataStore">The data store
        /// to remove</param>
        public void RemoveDataStore(
            IDataStore dataStore)
        {
            lock (this.dataStores)
            {
                if (this.dataStores.Contains(dataStore))
                {
                    this.dataStores.Remove(dataStore);
                }
                else
                {
                    //Note that the item was not found in the collection
                    //
                    LoggingTools.SendToLog("DataStore: " + dataStore.ToString() + ", was not found in the collection of dataStores.", LoggingCategories.GENERAL, 50);
                }
            }
        }

        /// <summary>
        /// Adds a view manager to the collection
        /// of view managers the controller is aware of
        /// </summary>
        /// <param name="viewManager">The view manager
        /// to add</param>
        public void AddViewManager(IViewManager viewManager)
        {
            lock (this.viewManagers)
            {
                if (!this.viewManagers.Contains(viewManager))
                {
                    this.viewManagers.Add(viewManager);
                }
            }
        }

        /// <summary>
        /// Removes a view manager from the collection
        /// of view managers the controller is aware of
        /// </summary>
        /// <param name="viewManager">The view manager
        /// to remove</param>
        public void RemoveViewManager(
            IViewManager viewManager)
        {
            lock (this.viewManagers)
            {
                if (this.viewManagers.Contains(viewManager))
                {
                    this.viewManagers.Remove(viewManager);
                }
                else
                {
                    //Note that the item was not found in the collection
                    //
                    LoggingTools.SendToLog("ViewManager: " + viewManager.ToString() + ", was not found in the collection of viewManagers.", LoggingCategories.GENERAL, 50);
                }
            }
        }
        
        /// <summary>
        /// Add an importer, if not already present, to the collection
        /// of importers for this controller
        /// </summary>
        /// <param name="importer">the importer</param>
        public void AddImporter(Sandia.MVCFramework.ExportTools.IImporter importer)
        {
            lock (this.importers)
            {
                if(!this.importers.Contains(importer))
                {
                    //Since it is not already in the collection, add it
                    //
                    importers.Add(importer);

                    //Now that it has been added - initialize it
                    //
                    importer.InitDataPacketProducer(this);
                }
            }
        }

        /// <summary>
        /// Remove an importer, if present, from the collection
        /// of importers for this controller
        /// </summary>
        /// <param name="importer">the importer</param>
        public void RemoveImporter(Sandia.MVCFramework.ExportTools.IImporter importer)
        {
            lock(this.importers)
            {
                if (this.importers.Contains(importer))
                {
                    //Since it is in the collection, remove it
                    //
                    importers.Remove(importer);

                    //No clean-up necessary
                }
                else
                {
                    //Note that the item was not found in the collection
                    //
                    LoggingTools.SendToLog("Importer: " + importer.ToString() + ", was not found in the collection of importers.", LoggingCategories.GENERAL, 50);
                }
            }
        }

        /// <summary>
        /// Add an exporter, if not already present, to the collection
        /// if exporters for this controller
        /// </summary>
        /// <param name="exporter">the exporter</param>
        public void AddExporter(Sandia.MVCFramework.ExportTools.IExporter exporter)
        {
            lock (this.exporters)
            {
                if (!this.exporters.Contains(exporter))
                {
                    //Since it is not already in the collection, add it
                    //
                    this.exporters.Add(exporter);

                    //Nothing to initialize
                }
            }
        }

        /// <summary>
        /// Remove and exporter, if present, from the collection
        /// of exporters for this controller
        /// </summary>
        /// <param name="exporter">the exporter</param>
        public void RemoveExporter(Sandia.MVCFramework.ExportTools.IExporter exporter)
        {
            lock (this.exporters)
            {
                if (this.exporters.Contains(exporter))
                {
                    //Since it is in the collection, remove it
                    //
                    this.exporters.Remove(exporter);

                    //No clean-up necessary
                }
                else
                {
                    //Note that the item was not found in the collection
                    //
                    LoggingTools.SendToLog("Exporter: " + exporter.ToString() + ", was not found in the collection of exporters.", LoggingCategories.GENERAL, 50);
                }
            }
        }

        #endregion
    }
}